✨ Add xmd prompt: turn a request into an approved executable Plan (#260) - #657
Conversation
|
|
||
| // The trusted host selects its own root provider by name. Document-level | ||
| // selection goes through the installation protocol; this is the host saying | ||
| // what it configured, which no document is composing around. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // what it configured, which no document is composing around. |
| yield* installPermissionMode(permissionMode); | ||
| // `xmd run` is the one command that has a terminal to give away. Help, | ||
| // document inspection and `xmd test` install no launcher, so a document that | ||
| // reaches <Session.Launch> under any of them refuses instead of spawning. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // reaches <Session.Launch> under any of them refuses instead of spawning. |
| }, | ||
| // Declared so `xmd run --help` lists it with every other option. The value is | ||
| // lifted out of argv by readEvalFlags before parsing — see eval-source.ts — | ||
| // so this field is never the source of the document. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // so this field is never the source of the document. |
| // The same object generation was configured from. The provider it | ||
| // installs is a fresh ordinary one — the generator's scope is already | ||
| // gone — but which agent it defaults to and what it may do were | ||
| // decided once, for the whole invocation. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // decided once, for the whole invocation. |
| // Refused here rather than with the other commands' inline refusal below, | ||
| // because that one is reached through the parse this branch exists to skip. | ||
| // An inline document is what `xmd prompt` sets out to write, so a caller who | ||
| // supplied one would otherwise watch it generate a different one instead. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // supplied one would otherwise watch it generate a different one instead. |
| yield* deps.installElicitation(); | ||
| // The same vocabulary the catalog just described. Validation and the | ||
| // catalog read one registry, so a component the generator was told about | ||
| // is one validation resolves. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // is one validation resolves. |
| *installElicitation() { | ||
| yield* Elicitation.around( | ||
| { | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| { at: "min" }, | ||
| ); | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| return yield* scoped(function* () { | ||
| yield* ensure(() => rm(dir, { recursive: true, force: true })); | ||
| yield* API.Env.around({ | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| }); | ||
| // The provider `API.Files` has no host default for, installed exactly where | ||
| // the runtime entrypoint installs it: a document that reaches the | ||
| // filesystem must reach the caller's, or fail. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // filesystem must reach the caller's, or fail. |
PR #657: ✨ Add
|
`xmd prompt "<request>"` asks the configured ACP agent for a complete executable Markdown root, validates it without executing any of it, repairs its definite defects by asking again, shows a person the exact bytes, and runs the approved ones through the ordinary supplied-source path under the `<prompt>` identity. It adds authorship around one normal document; it adds no second execution model, props model or journal. Authorship sits outside durability. The catalog, the fresh generator session, every repair and the review create no journal and replay nothing, and the generator's scope closes before the optional exclusive `--save` and before execution. A defect the draft authored is repairable and earns one of three turns; a defect the command line authored terminates without spending a turn on something the agent cannot fix. `xmd run` and `xmd prompt` now share one execution field set, one resolved Agent configuration, and one props source resolution. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
Three review findings on the xmd prompt command. An inline document reached the generator. `preparePropsPhase` skips the parse that carries the other commands' `-e` refusal, so a supplied document was dropped in silence and the command generated a different one. Refused in prompt's own branch, before the catalog, the provider, the review, the save, the journal or any execution. The agent configuration was resolved twice, once for generation and again for the document, so one command line had two chances to read DEFAULT_AGENT_NAME differently. A dispatch now settles it once and hands the resolved value to both consumers; DocumentMode carries the settled stack rather than the flags. The executed program still gets a fresh ordinary provider and inherits neither the generator session nor its system prompt. A document failing its own <Testing> boundary printed a bare message where a run prints a `tests failed:` heading. reportFailure moves to a module both commands share. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
The first document-backed command needs its Markdown where the command is: a source checkout, a published npm package, and a compiled binary with no checkout. The document is the deployed program, so it ships as a file rather than as a generated string. src/documents/prompt-plan.md is that program. A package declares what it ships by putting it in that directory; test documents and scenario fixtures live under src/ too, and a build that swept those up would publish fixtures and grow every binary carrying them. The command locates it from its module's own URL, never from the working directory and never through the component search path, so it finds the same policy whatever directory a person stands in and no repository file can answer for it. Each build keeps the directory beside its module: both deno compile sites embed it, and the npm build copies it into the emitted tree, because dnt emits the module graph only and an asset nothing imports would otherwise reach Node and Bun missing while Deno stayed green. A regression holds the two compile sites to the documents that exist, since that list is the one thing no build discovers. The host still runs the TypeScript authorship loop; wiring the plan program into the plan profile follows. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
A <Return> selects the value a value body publishes; it does not end that body, and a later failure wins over a value already selected. The approve branch returned the candidate and broke out of the review loop, then execution left the Session and reached the unconditional exhaustion <Fail>. Every approval settled as the ten-draft failure and no approved candidate could reach the host. Approval now binds the exact candidate to a null-sentinel result, and one exhaustive branch after the Session either returns it or fails. Only the arm taken runs, so an approved candidate has no later sibling failure to overtake it. Bounds, prompt wording, diagnostics, presentation, Session structure and the authored abort remain as they were. The review schema also needed an explicit string type: Ajv strict mode refuses minLength on an untyped property, so every review was refused before a person saw anything. Diagnostics reach their code block through <Json> rather than a JSON.stringify call in an expression, so serializing them is a document construct like everything else this policy does. The regression runs the packaged document itself, resolved against first-party declarations with an empty include list, against a scripted agent turn, a scripted approval and a test validator. It fails with the exhaustion message before this change. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
`xmd prompt` now executes two roots with a scope boundary between them. First `src/documents/prompt-command.md`, the checked-in first-party Markdown that implements the conversion and its review workflow; then the Plan it returned, through the ordinary supplied-source path. The TypeScript authorship loop — `author()`, `evaluate()` and `review()` — is gone: what the assistant is asked, how many drafts may be fixed, how many you are shown and what happens when you approve nothing are readable Markdown now, and arguable. A Plan is not a script with comments. The shipped generation, repair and revision instructions ask for every requested outcome as reader-facing prose with each component placed immediately after the sentences describing what it does, so the source can be audited and the execution followed by the same narrative. That authorship rule is the document's and human review's; it is never a hidden validation rule. The prompt profile is what that document runs inside. The assistant gets an empty directory of this host's own, no additional directories, no MCP servers, no native tools and a strict denial answered inside the provider — so nothing composed around it can widen a ceiling with nothing in it, and `--approve-all` configures the approved Plan rather than the conversation that wrote it. The document itself reaches no Files, command, service or network capability, runs against no repository component search, and cannot opt out of a failing turn ending it: a turn that streamed half a program and failed presents nothing. `<ValidateCandidate>` is declared to that execution rather than resolved from a repository, and it keeps the two kinds of failure apart. A defect a draft authored comes back as structured facts the document may repair; a defect the command line authored raises out of it, so no policy can catch a caller's mistake and call it feedback. The host takes the returned Plan back as untrusted text. Only after every Prompt task, provider and Elicitation resource has torn down does it validate those exact bytes again and resolve their props — a revision that changed a property's declared type changes what the run receives — and only then does `--save` create the file and the Plan run. `--session <name>` names the assistant session; without it each invocation places its own. The profile's working directory is one fixed host-owned path because a session's key includes the directory it lives in, and a location that changed every time would leave that option unable to name anything. Evidence runs the packaged document itself against a scripted agent and a scripted review: the shipped words reach the turn, ten presentations bound the review with no revision on the tenth, an interleaved Plan returns byte for byte, and what a person reads says each thing once however many rounds it took. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
…on (#260) Four corrections to the prompt profile and the command document it runs. One directory per logical session, at `~/.xmd/prompt/sessions/<sha256(logical-session-name)>`, replaces the single `~/.xmd/prompt` every invocation shared. A shared directory was one ambient location two conversations could both stand in; a fresh one each time would have left `--session` unable to name anything, since a session's key includes the directory it lives in. The digest gives both: a generated name reaches a location nothing else does, and the same explicit name reaches the same one, so the provider continues the session it established rather than placing a second. The leaf is the digest and never the name — a caller's string that becomes a path is a caller's string that can escape one. Emptiness is now proven rather than assumed. The directory is created empty and required to be empty before the provider is constructed or a session is materialized, and anything already in there is a terminal refusal naming the path. Nothing is deleted: what is in a directory this host did not authorize anything to write to is not this command's to clean up. Stopping on the tenth draft says two different things, and now the document says both. When that draft still has problems, `abort` is the only choice offered and there was never a Plan to approve, so it routes to the authored exhaustion message. Every other abort — including one on a tenth draft that could have been approved — stays the ordinary ending. The branch after the Session remains an exhaustive fallback. Final admission is now shown to be independently effective. A scripted draft uses a repository component that exists while the command document runs and is removed as its scope tears down; the unchanged approved bytes then fail the host's second validation, in that order, with no save, journal or execution. Both validations run the production path — leaving the component in place makes the case pass and execute, which is what makes it evidence. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
…ry back (#260) Two ownership corrections: one about which directories a test may touch, and one about which directories an invocation may keep. The suite no longer reads, creates or removes anything under a real `~/.xmd/prompt`. Snapshotting that root and deleting whatever appeared after the snapshot was unsafe on its face — absence from a snapshot says nothing about who created a directory, so an overlapping case or a real invocation could have its state removed by a test that never owned it. Where profile session directories live is now a host dependency: production keeps its default, and a harness names a tree it made itself, passed as a value with no flag, environment variable, document prop or replaceable context anywhere near it. `createPromptHarness` requires that root, so no case can fall back to the host's, and each root is a per-scope temporary directory removed whole — which is safe precisely because the scope created everything in it. The one case that needs two invocations to share a root says so explicitly. Only an explicitly named session needs its directory afterwards. Its identity is what a later `--session` derives the same ACPX session from, so it stays, is required empty on the way in every time, and is never cleaned. An invocation-unique default names nothing anybody can ask for again, so its directory belongs to the command: after the command document and every provider, Prompt task and Elicitation resource inside it has torn down, exactly one cleanup is attempted, and it settles before final admission, the save or the execution begins. Still empty, and the leaf is removed non-recursively. No longer empty, and the directory and its contents are preserved and the command fails terminally — this host authorized nothing to write there, and deleting a stranger's files to get on with the work is the opposite of what a ceiling is for. Being an `ensure` is what makes an abort, a failed turn and a cancellation settle the same way a success does. Which of the two applies is a trusted host value — whether the caller wrote `--session` — never inferred from the shape of the generated name, and never visible to the command document. Both outcomes are held by evidence that fails without them: disabling the cleanup leaves the leaf behind on success and before admission, and making the removal recursive turns the preserved-and-refused case into a silent success. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
… making it (#260) Rebased onto `<Json as>` (#667), which supplies what the single-child `<Let>` wrapper was standing in for. Both wrappers in the packaged prompt command document are gone; `<Json value={check.diagnostics} as="problems" />` binds the serialized text directly, and the bytes reaching `<CodeBlock>` are the same bytes — the repair turn and the review presentation still carry the complete structured problems, now proven by parsing the fence back rather than by matching a substring of it. The default session's directory is now a scope-owned resource rather than a directory with an ensure attached afterwards. The release is registered before the `mkdir` that could create it, so there is no window in which a leaf exists that nothing is responsible for: a failure between making it and using it hands it back like every other ending. What the release knows is whether the directory was ever handed over, and that decides which question it is answering. Once established, the exit has exactly two outcomes and no third. Still the empty directory it was given, and the leaf is removed, non-recursively. Anything else — content that appeared, or a directory that vanished — is preserved as found and fails the command terminally. A leaf that disappears under a live conversation was silently accepted before, which was wrong twice over: nothing here is allowed to remove it, so its absence is interference, and treating interference as a clean exit would let admission, the save and the execution proceed on a conversation something else had already reached into. A directory establishment never handed over is a different question, already answered by what establishment reported. The release leaves that report and the directory's contents alone, while still handing back an empty leaf it did create. Explicitly named directories are untouched by any of this: they stay, because the next `--session` derives the same ACPX session identity from where they are. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
… end (#260) The command document is now a workflow somebody can read straight through. It opens by saying what `xmd prompt` is for — a Prompt describes steps, the components carry them out, and the coding agent turns both into one document that explains and executes the sequence — and every stage after that is a heading: creating the first draft, checking and repairing it, reviewing it, continuing from the decision, returning the approved Plan. Every Plan now begins with one descriptive level-one title, and the generation, repair and revision turns each ask for it alongside the steps-beside-components structure, so a replacement cannot quietly drop either. The worked example is a titled Plan itself. This is an authorship and human-review requirement, not a rule hidden in the checker: `<CheckDraft>` reports structural facts and would accept a titleless Plan that a person would send back. `<ValidateCandidate>` is `<CheckDraft>` throughout. It checks a draft rather than validating a candidate, and nothing about what it does changed. The review choices are the words a person reads — Approve, Request changes, Stop — with no internal spelling behind them. A tenth draft that still has problems now offers something better than only stopping: Explain what went wrong makes one more ordinary turn in the same Session, carrying only the final diagnostics, because the conversation already holds the Prompt, the catalog and every draft. It asks for an explanation and explicitly not another Plan; its answer is inert text, reopens no draft limit, is reported as the coding agent's own words, and ends the command. So the host's instruction layer no longer demands that every answer be document source — it says only that an answer belongs to the message that asked for it, and each authored turn owns its own shape. The approved Plan is the command's result. By default it goes to stdout, byte for byte, with nothing this command added, so it can be piped, diffed or read before anybody commits to it. `--output` puts those bytes in a file instead, exclusively created. `--run` runs it. With both, the file is written first and only a successful write is followed by the run. `--save` is gone rather than aliased; nothing has been released to keep compatible with. That makes most execution flags conditional, so they are refused rather than ignored: a caller who asks for a journal, a permission mode or an exec deadline from a command that runs nothing has not been answered. `--include`, `--agent-provider`, `--default-agent`, `--session` and `--timeout` keep working always — they build the catalog, settle the agent, name the conversation, admit properties and bound the command. A journal exists only when `--run` begins. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
) A repair or a revision was told to "keep" the draft's title. That is the wrong instruction for a replacement: a draft whose title is missing, in the wrong place, or does not describe the Plan has nothing worth keeping, and an instruction to keep it asks the coding agent to carry the defect forward. Each Plan-producing turn — the first draft, each repair, each revision — now states the complete requirement on its own: optional frontmatter and then one descriptive level-one heading as the first body content, the Prompt's complete sequence as readable steps, every requested outcome, a meaningful order, and each component beside the prose describing what it does. Replacements are told to write the title the Plan needs rather than the one the last draft had. None of that reaches `<CheckDraft>`, `validateDocument()` or TypeScript. It is what the workflow asks for and what a person reviews, which is where a judgement about whether a title describes a Plan belongs. The prose under "Continue from your decision" claimed a final invalid draft left only one choice. It leaves two: ask the coding agent what went wrong, or stop. The governing documents still described the earlier command. They were swept deliberately rather than by replacing words: the opening example is a titled Plan, the flow shows the four destinations, and the failure table, timeout paragraph, acceptance rows, terminology rows, construct inventory, executable-MDX identity section, ACP lifecycle paragraph and root-props admission paragraph no longer say that an approved Plan always runs, that `--save` is the output option, that stopping is called abort, that admission is followed unconditionally by execution, or that every success writes a journal. The evidence follows the same rule. The approved introduction is pinned whole, punctuation included, rather than sampled; the requirement block is asserted against the actual initial, repair and revision turns and counted three times in the shipped source; and the run-only preflight table covers every spelling, including `-V` and `--secret-detection`. The test-local `validateCandidate` is `checkDraft`, so a terminology sweep finds no survivor of the old name. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
The introduction claimed to be the approved wording and was one byte away from it: `coding agent's plan` with an ASCII apostrophe where the contract has U+2019. The regression that was supposed to catch that had copied the same byte, so it pinned the file to itself rather than the file to the contract. Both now carry E2 80 99, and the fixture's comment says why it is the byte and not the sentence that is being held. Two contract passages still described the earlier command. The ACP lifecycle said the profile's scope closes before "the save and the execution"; it closes before final admission and before whichever result was asked for. The command spec said `xmd prompt` executes two root documents; it always executes the command document and executes a second only under `--run`. The terminology sweep reached the comments, messages and test names that the earlier passes left behind: "the save" in the deadline and teardown comments, the harness's `--save`, the profile's two directory failures now saying "nothing was output or run", and the review-decision comments and test names that still called stopping an abort. The unrelated abort in `runDocument` is left alone — it is about a document's own failure, not this command's. `--save` had no real proof it was gone. The case that passed it also passed two mutually exclusive permission flags, so the earlier failure decided the outcome and the assertion would have held whether or not the option still worked. That case no longer carries it, and a focused one does — on a command line that is otherwise entirely valid. Making that case honest exposed something worth fixing. `--save out.md` was refused, but as "unrecognized argument: out.md": the scanner passed the unknown option to a parser that stops at the first option it does not define and drops the rest, so what a caller heard about was the value rather than the flag. Fixed preflight now refuses an option this command does not define, by name, and names `--output` when the option was `--save`. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
The module contract opened by claiming `xmd prompt` owns two root document executions, and its flow diagram ended by running the second one unconditionally — one line after the line listing stdout, an `--output` file, a run, or both as the four ways the bytes are delivered. A reader following the diagram would conclude that a default invocation runs the Plan it just printed. Every invocation executes the packaged prompt command document. A second root is executed only under `--run`. The diagram now shows the four deliveries as the alternatives they are, and the paragraph after it puts the complete scope boundary where it belongs: before that optional second execution, so whatever result follows has already let go of the conversation that wrote it. The same claim opened the prompt profile's header and the Tier PR introduction, and three nearby comments still had execution as the thing that follows approval rather than one of the things that might. Corrected to the approved bytes, or to the run that may follow. Prose only: no runtime behaviour, no tests and no evidence changed. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
edb680f to
142272d
Compare
xmd prompt: turn a request into an approved executable Plan (#260)
| }, | ||
| // Declared so `xmd run --help` lists it with every other option. The value is | ||
| // lifted out of argv by readEvalFlags before parsing — see eval-source.ts — | ||
| // so this field is never the source of the document. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // so this field is never the source of the document. |
| stream: new InMemoryStream(), | ||
| // No repository component search. What the document may name is | ||
| // what this profile declares, so a file in the caller's tree cannot | ||
| // answer for `<CheckDraft>`, `<Prompt>` or anything else. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // answer for `<CheckDraft>`, `<Prompt>` or anything else. |
| // The command document lives and dies inside that call's scope. Leaving it | ||
| // closes the Prompt tasks, the provider and the Elicitation provider, so a | ||
| // teardown failure raises out here — before the admission, the output file | ||
| // and the run that would otherwise already have happened. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // and the run that would otherwise already have happened. |
| const source = authored.value; | ||
| if (command.output !== undefined) { | ||
| // Before the run, so a Plan that fails at run time is still on disk to read | ||
| // and hand-edit. An existing path is refused and nothing after it happens. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // and hand-edit. An existing path is refused and nothing after it happens. |
| // The approved Plan is the result. It goes to stdout exactly as the agent | ||
| // wrote it — no fence, no heading, no trailing newline of this command's — | ||
| // so a caller can pipe it into a file, a diff or another program. A caller | ||
| // who named `--output` already has it, and gets a quiet command instead. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // who named `--output` already has it, and gets a quiet command instead. |
|
|
||
| // Whether the root declares itself readably. Inspection would raise on a | ||
| // malformed declaration, and recovering a code from an exception's prose is | ||
| // exactly what the structured answer exists to replace. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // exactly what the structured answer exists to replace. |
| const dir = join(tmpdir(), `xmd-prompt-${randomUUID()}`); | ||
| // A sibling rather than a child: the working directory is what the approved | ||
| // document writes into and what several cases read back, and a profile root | ||
| // inside it would show up in those listings. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // inside it would show up in those listings. |
| yield* ensure(() => rm(dir, { recursive: true, force: true })); | ||
| // Recursive, and safe because it is: everything under this root was created | ||
| // by this scope, so nothing here can reach a directory another case or a | ||
| // real invocation owns. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // real invocation owns. |
| }); | ||
| // The provider `API.Files` has no host default for, installed exactly where | ||
| // the runtime entrypoint installs it: a document that reaches the | ||
| // filesystem must reach the caller's, or fail. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // filesystem must reach the caller's, or fail. |
An option's name is read up to its first `=`, so `--run=false` arrived under the name of the switch and was taken for it. That set the flag saying execution had been requested, which is also what satisfies the gate refusing options that only configure a run — so `--run=false --journal trace.jsonl` was accepted. What followed is worth stating precisely, because it is not what the flag being set suggests. The ordinary parser reads `--run=false` as false and `--run=true` as the field's default, which is also false. So the command took the write path either way: `--run=false --journal <path>` completed successfully, created no journal, and never told the caller that the journal they asked for was not a thing this invocation would produce; and `--run=true` was silently answered by a command that wrote the Plan instead of running it. One half of the command believed a run had been requested and the other half did not, and neither told anybody. Every valued spelling — `--run=false`, `--run=true`, `--run=` — now fails fixed preflight, before it establishes that execution was requested and before it can answer for the gate. Nothing is loaded, built, contacted, opened, created or executed. The regressions fail without it: the scanner cases find no error, and the command-level case exits 0 where it now exits 1 — the old behaviour was a success, not an incidental failure, which is what makes the tripwires worth asserting. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
…260) The refusal has to prove authorship never began, and the two tripwires that say so most directly were implied rather than asserted. A provider count of zero already rules out an established session, but the claim worth making is the narrow one: no session was established, and no directory was made for one to run in. The absence of an output file was likewise folded into an empty working directory rather than stated. Evidence only: no behaviour, no message and no other case changed. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
The defect was a disagreement between two layers: the scanner read `--run=false` as the switch while the parser read it as false. The command-level regression for it hand-built the scanned result and called `runPrompt` directly, so it never reached `preparePropsPhase`, the parser or dispatch — the boundary the bug lived on. It would have passed with the two layers still disagreeing. A real invocation now covers it, for `--run=false`, `--run=true` and `--run=`: the exact refusal, empty stdout so no approved Plan escaped, no unavailable-agent error because reaching a provider is what produces one, and an empty working directory holding neither the journal it named nor anything else. Both existing cases stay, because the three prove different things. The scanner table is the grammar. This one is the boundary. The direct one names the phases that stayed at zero, which a subprocess cannot see. Without the refusal this case fails at `Expected: 1 / Received: 0`, and its stderr carries the unavailable-agent error — the command line reached a provider, which is the authorship this refusal exists to prevent. Evidence only: no production change. Claude-Session: https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6
| }, | ||
| // Declared so `xmd run --help` lists it with every other option. The value is | ||
| // lifted out of argv by readEvalFlags before parsing — see eval-source.ts — | ||
| // so this field is never the source of the document. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // so this field is never the source of the document. |
| // The same object generation was configured from. The provider it | ||
| // installs is a fresh ordinary one — the profile's scope is already | ||
| // gone — but which agent it defaults to and what it may do were | ||
| // decided once, for the whole invocation. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // decided once, for the whole invocation. |
| // silence — and a caller who asked for something the command never did | ||
| // has not been answered. `--save` is named because it is the one | ||
| // spelling somebody may remember; it was replaced before release, so | ||
| // there is no alias to keep. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // there is no alias to keep. |
| // declared it. Frozen while the Plan is being written and carried into the | ||
| // final gate, so the bytes that are delivered are checked against the command | ||
| // line that was written rather than against whichever draft happened to be | ||
| // last. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // last. |
| // emitted module. dnt emits the module graph and nothing else, so an asset no | ||
| // TypeScript imports would be absent from the published package while the | ||
| // source checkout kept working — the command would find nothing at runtime, | ||
| // on Node and Bun only. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // on Node and Bun only. |
Closes #260.
Why
xmd run -eexecutes a program you already wrote.xmd promptis the step before that: you describe the outcome, and a readable Executable Markdown workflow turns it into a Plan — a document that states what you asked for in ordinary language and places the components that do the work alongside those words.xmd prompt "ask me for my age and write it to a file"The point is that the authorship policy is not in TypeScript. What the agent is asked, how many times a draft may be repaired, what you are shown and what happens when you approve nothing all live in one packaged Markdown document you can read and argue with.
What changes
Before:
After — the approved Plan is the result, printed to stdout by default:
Four delivery modes, exactly one per invocation: source on stdout (default);
--output <path>exclusively creates a file;--runexecutes the Plan;--output --runwrites the file, then runs it.How it works
Every invocation executes one root document — the packaged prompt command document — and a second only under
--run:The prompt profile is a constrained host context: a per-session working directory derived as
sha256(session name), proven empty before the provider is built, no additional directories, no MCP servers, no native tools, strict denial. Permission flags configure the approved Plan, not authorship, and cannot widen it.A draft is inert text throughout. It is checked without being executed, and only after approval, complete teardown and a second host validation can those bytes be delivered or run.
Review guide
Start with:
packages/cli/src/documents/prompt-command.md— the whole authorship policy, in the open.Then review:
packages/cli/src/prompt-profile.ts— the ceiling, the session directory lifecycle, and the host-declared<CheckDraft>.packages/cli/src/prompt.ts— teardown, final admission, and the four delivery modes.packages/cli/src/prompt-args.ts— fixed grammar, option-signature freezing, run-only flag refusal.packages/cli/src/packaged-document.ts+scripts/build-npm.ts— how the document reaches every build.Look carefully at:
<Return>selects a value; it does not end the body. Approval and exhaustion are the two arms of one<If>/<Else>for exactly this reason — an earlier version returned the draft and then hit a later<Fail>, so every approval failed.ENOENT) → preserved untouched, command fails terminally. Nothing is ever recursively deleted, and named sessions are exempt because continuation depends on their identity.<CheckDraft>and ends the command. Getting this backwards teaches the agent about mistakes it cannot fix.What must stay true
<Prompt>or the Agent API — enforced by the packaged document owning every branch, checked by assertions that count the requirement block three times and pin the introduction whole.--outputand execution.--runbegins.import.meta.url, never cwd, never the component search path.How to verify it
At
142272da: focused suites5 passed (40 steps); typecheck, lint, format andgit diff --checkclean. The affected graph passed at1e449879with830 passed (5556 steps), 0 failed, and nothing executable has changed since — the four commits after it touch prose, tests and CLI grammar only.Portability is proven per build, because a document read at runtime fails differently in each: the loader suite runs under Deno, Node and Bun;
scripts/tests/cli-npm-bin.test.tsbuilds the real npm package and assertsesm/src/documents/prompt-command.mdis byte-identical;scripts/tests/packaged-document.test.tsholds bothdeno compile --includesites to the documents that exist; and the compileddist/xmdwas run by absolute path from an empty temp cwd withHOMEredirected, reaching an ACP probe — which is only possible after reading the embedded document and starting a turn.Several regressions were confirmed to fail without their fix, since one that passes either way proves nothing: removing the cleanup
ensure, replacingrmdirwith a recursive remove, revertingENOENTto a silent return, and retargeting the<CheckDraft>teardown to a nonexistent file all turn their case red.Scope
Included
xmd promptcommand, its grammar, help and four delivery modes;<prompt-command>execution;<CheckDraft>capability and the post-teardown admission gate;specs/prompt-command-spec.mdplus updates to four existing specs andarchitecture.md.Intentionally unchanged
<Prompt>is still exactly one Agent turn. Nothing in core changed to accommodate this command.<Plan>, custom command documents,--plan/--plan-evalor File/Glob authorship context — those are 🏛️ Compose Plan authorship through a reusable <Plan> component #660.--save; it was unreleased and is refused by name rather than aliased.New abstractions
prompt-args.ts— the request/property boundary is decidable from fixed grammar alone, and proving every refusal is effect-free needs a pure function over argv.agent-stack.ts—runandprompttake the same Agent options and must not reach different conclusions from one command line.prompt-profile.ts— the constrained context and the trusted checker declared into it.packaged-document.ts— two functions locating a shipped document from its own module URL.report.ts— both commands end in one ordinary execution and should report failure identically.Risks and limitations
mkdircannot be discriminated by a black-box test — a default session's establishment cannot fail aftermkdirwithout adding a production seam, which was deliberately not added. Everything downstream is covered.--sessioncontinuation is proven against a shared in-memory ACPX store, not a live provider.<Fail>after the loop is a defensive fallback and says so; exhaustion is decided inside review.xmd promptmust be the first argument, asxmd workflowmust, with an explicit refusal otherwise.xmd run's Agent flag errors surface marginally earlier now, since both commands share one settledAgentStack.Scope confirmation
https://claude.ai/code/session_015HcqB9kJM9KFnMNToAuZF6